/* eslint-disable @typescript-eslint/no-explicit-any */
import { cls } from '@knno/jsx';
import { contentEditable } from '../../contentEditable';
import { ALLOW_CONTROLS, setRangeByPos, setSelection } from '../../editor';
import { cellStyle } from '../cell.css';
import { inlineStyle } from '../inline.css';
import { InputBase, type InputBaseProps } from '../inputBase';
import { numberInputStyle } from './number.css';

const ALLOW_CHARACTERS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '-']);

function allowInsertContent(elem: HTMLElement, str: string): boolean {
	const sel = window.getSelection();
	if (!sel || sel.rangeCount === 0) return false;
	const range = sel.getRangeAt(0);
	const originalText = elem.textContent ?? '';
	const startIndex = range.startOffset;
	const endIndex = range.endOffset;
	const newText = originalText.slice(0, startIndex) + str + originalText.slice(endIndex);
	const cleanedText = newText.replaceAll(/,/g, '');
	return !isNaN(Number(cleanedText));
}

function removeNonAscii(elem: HTMLElement) {
	const sel = window.getSelection();
	if (!sel || sel.rangeCount === 0) return false;
	const range = sel.getRangeAt(0);
	if (range.startContainer !== range.endContainer || range.startContainer !== elem.firstChild) {
		return false;
	}
	const originalText = elem.textContent ?? '';
	const startIndex = range.startOffset;
	const endIndex = range.endOffset;
	const firstPart = originalText.slice(0, startIndex).replaceAll(/[^0-9-.]/g, '');
	const secondPart = originalText.slice(endIndex).replaceAll(/[^0-9-.]/g, '');
	const text = firstPart + secondPart;

	elem.textContent = text;
	if (elem.firstChild) {
		range.setStart(elem.firstChild, firstPart.length);
		range.collapse(true);
	}
}

export type NumberInputProps<T extends NumberInputBase<any, any, any>> = InputBaseProps<
	T,
	number
> & {
	maxPrecision?: number;
	minPrecision?: number;
	prefix?: string;
	suffix?: string;
	thousandsComma?: boolean;
	/**
	 * min and max are only used to validate the input value.
	 * They are not used to format the input value.
	 */
	max?: number;
	min?: number;
};

abstract class NumberInputBase<
	P extends NumberInputProps<C>,
	C extends NumberInputBase<P, C, E>,
	E extends HTMLElement,
> extends InputBase<P, C, E, number> {
	constructor(props: P) {
		super(props);
		this.setMaxPrecision(this.maxPrecision);
		this.setMinPrecision(this.minPrecision);
		this.setPrefix(this.prefix);
		this.setSuffix(this.suffix);
		this.setThousandsComma(this.thousandsComma);
		this.el.tabIndex = 0;
		this.el.classList.add(numberInputStyle);
		this.el.addEventListener('input', () => {
			removeNonAscii(this.el);
			this.updateValue(this.parseNumber());
		});
		this.el.addEventListener('keydown', (evt) => {
			if ((evt.ctrlKey || evt.metaKey) && /^[acvxz]$/.test(evt.key)) return;
			if (ALLOW_CHARACTERS.has(evt.key)) {
				if (!allowInsertContent(this.el, evt.key)) {
					evt.preventDefault();
				}
			} else if (!ALLOW_CONTROLS.has(evt.key)) {
				evt.preventDefault();
			}
		});
		let middleButtonDown = false;
		let mouseDown = false;
		this.el.addEventListener('mouseup', (evt) => {
			mouseDown = false;
			if (evt.button === 1) {
				evt.preventDefault();
				setTimeout(() => {
					middleButtonDown = false;
				});
			}
		});
		this.el.addEventListener('mousedown', (evt) => {
			mouseDown = true;
			if (evt.buttons === 4) {
				// only middle button
				middleButtonDown = true;
				evt.preventDefault();
				this.el.focus();
				//this.format();
				setTimeout(() => {
					const range = document.createRange();
					range.selectNodeContents(this.el);
					if (!this.el.textContent) {
						range.collapse(true);
					}
					const sel = window.getSelection();
					if (sel) {
						sel.removeAllRanges();
						sel.addRange(range);
					}
				});
			}
		});
		this.el.addEventListener('dragover', (evt) => {
			// evt.preventDefault();
			// if call preventDefault caret will disappear
			evt.stopPropagation();
			setRangeByPos(evt.clientX, evt.clientY);
		});
		this.el.addEventListener('drop', (evt) => {
			evt.preventDefault();
			evt.stopPropagation();
			const clipboardData = evt.dataTransfer;
			if (!clipboardData) return;
			this.paste(clipboardData);
		});
		this.el.addEventListener('paste', (evt) => {
			evt.preventDefault();
			if (middleButtonDown) return;
			const clipboardData = evt.clipboardData;
			if (!clipboardData) return;
			this.paste(clipboardData);
		});
		this.el.addEventListener('blur', () => {
			this.format();
		});
		this.el.addEventListener('focus', () => {
			if (this.readOnly) return;
			setTimeout(() => {
				if (!mouseDown) {
					const range = document.createRange();
					if (this.el.textContent && this.el.firstChild) {
						this.el.textContent = this.el.textContent?.replaceAll(/[^0-9-.]/g, '') ?? '';
						range.selectNodeContents(this.el.firstChild);
						setSelection(range);
					}
				} else {
					removeNonAscii(this.el);
				}
			});
		});
	}

	private format() {
		if (this.value === undefined) {
			this.el.classList.add('empty');
			return;
		}
		this.el.classList.remove('empty');
		if (document.activeElement === this.el) {
			removeNonAscii(this.el);
		} else {
			const value = this.value;
			if (isNaN(value)) {
				this.el.classList.add('empty');
				this.el.textContent = '';
				this.updateValue(undefined);
			} else {
				const formattedValue = this.thousandsComma
					? value.toLocaleString('zh-CN', {
							minimumFractionDigits: this.minPrecision,
							maximumFractionDigits: this.maxPrecision,
						})
					: value.toString();
				this.el.textContent = formattedValue;
			}
		}
	}

	protected override renderContent(): void {
		this.el.textContent = this.value ? this.value + '' : '';
		this.setMaxPrecision(this.maxPrecision);
	}

	protected override changeReadOnly(): void {
		if (!this.props.readOnly) {
			this.el.setAttribute('contenteditable', contentEditable);
		} else {
			this.el.removeAttribute('contenteditable');
		}
	}

	private paste(clipboardData: DataTransfer) {
		if (this.readOnly) return;
		if (clipboardData.types.includes('text/plain')) {
			let txt = clipboardData.getData('text/plain');
			txt = txt.replace(/,/g, '');
			// if (isNaN(Number(txt))) {
			// 	return;
			// }
			if (allowInsertContent(this.el, txt) && txt) {
				const sel = window.getSelection();
				if (!sel || sel.rangeCount === 0) return;
				const range = sel.getRangeAt(0);
				range.deleteContents();
				range.collapse();
				range.insertNode(document.createTextNode(txt));
				range.collapse();
				this.el.querySelectorAll('br').forEach((br) => br.remove());
			}
			this.updateValue(this.parseNumber());
		}
	}

	private parseNumber(): number | undefined {
		if (this.el.textContent === null || this.el.textContent.length === 0) {
			this.el.classList.add('empty');
			return undefined;
		}
		this.el.classList.remove('empty');
		let txt = this.el.textContent;
		txt = txt.replaceAll(/,/g, '');
		const value = Number(txt);
		if (isNaN(value)) {
			this.el.classList.add('empty');
			this.el.textContent = '';
			return undefined;
		}
		if (this.maxPrecision !== undefined) {
			return Number(value.toFixed(this.maxPrecision));
		}
		return value;
	}

	get max(): number | undefined {
		return this.props.max;
	}
	setMax(value: number | undefined): this {
		this.props.max = value;
		return this;
	}
	get min(): number | undefined {
		return this.props.min;
	}
	setMin(value: number | undefined): this {
		this.props.min = value;
		return this;
	}

	get prefix(): string {
		return this.props.prefix ?? '';
	}
	setPrefix(prefix: string): this {
		this.props.prefix = prefix;
		this.el.setAttribute('data-prefix', prefix);
		return this;
	}
	get suffix(): string {
		return this.props.suffix ?? '';
	}
	setSuffix(suffix: string): this {
		this.props.suffix = suffix;
		this.el.setAttribute('data-suffix', suffix);
		return this;
	}

	get maxPrecision(): number | undefined {
		return this.props.maxPrecision;
	}

	setMaxPrecision(precision: number | undefined): this {
		if (precision !== undefined) {
			if (precision < 0) {
				precision = 0;
			} else if (precision > 20) {
				precision = 20;
			}
		}
		this.props.maxPrecision = precision;
		const newValue = this.parseNumber();
		if (newValue !== this.value) {
			this.updateValue(newValue);
		}
		if (
			this.minPrecision !== undefined &&
			precision !== undefined &&
			precision < this.minPrecision
		) {
			this.setMinPrecision(precision);
		} else {
			this.format();
		}
		return this;
	}
	get minPrecision(): number | undefined {
		return this.props.minPrecision;
	}
	setMinPrecision(precision: number | undefined): this {
		if (precision !== undefined) {
			if (precision < 0) {
				precision = 0;
			} else if (precision > (this.maxPrecision ?? 20)) {
				precision = this.maxPrecision ?? 20;
			}
		}
		this.props.minPrecision = precision;
		this.format();
		return this;
	}

	get thousandsComma(): boolean {
		return this.props.thousandsComma ?? false;
	}
	setThousandsComma(value: boolean): this {
		this.props.thousandsComma = value;
		return this;
	}
}

export type NumberCellProps = NumberInputProps<NumberCell> & {
	colSpan?: number;
	rowSpan?: number;
};

export class NumberCell extends NumberInputBase<NumberCellProps, NumberCell, HTMLTableCellElement> {
	protected render(): Node {
		return (
			<td
				class={cls(this.props.class, cellStyle)}
				style={this.props.style}
				colSpan={this.colSpan}
				rowSpan={this.rowSpan}
			/>
		);
	}
	get colSpan(): number {
		return this.props.colSpan ?? 1;
	}

	setColSpan(value: number): this {
		this.props.colSpan = value;
		this.el.colSpan = value;
		return this;
	}

	get rowSpan(): number {
		return this.props.rowSpan ?? 1;
	}

	setRowSpan(value: number): this {
		this.props.rowSpan = value;
		this.el.rowSpan = value;
		return this;
	}
}

export class NumberInline extends NumberInputBase<
	NumberInputProps<NumberInline>,
	NumberInline,
	HTMLSpanElement
> {
	protected render(): Node {
		return <span class={cls(this.props.class, inlineStyle)} style={this.props.style} />;
	}
}
